#include "common/pch.h"

#include "map/gridmap.h"
#include "map/griddefines.h"
#include "dbc/structure.h"

char const* MAP_MAGIC = "MAPS";
char const* MAP_VERSION_MAGIC = "z1.5";
char const* MAP_AREA_MAGIC = "AREA";
char const* MAP_HEIGHT_MAGIC = "MHGT";
char const* MAP_LIQUID_MAGIC = "MLIQ";

static unsigned short holetab_h[4] = { 0x1111, 0x2222, 0x4444, 0x8888 };
static unsigned short holetab_v[4] = { 0x000F, 0x00F0, 0x0F00, 0xF000 };

bool IsAcceptableClientBuild(unsigned int build)
{
    int accepted_versions[] = EXPECTED_MANGOSD_CLIENT_BUILD;
    for (int i = 0; accepted_versions[i]; ++i)
        if (int(build) == accepted_versions[i])
        {
            return true;
        }

    return false;
}

GridMap::GridMap()
{
    m_flags = 0;

    // Area data
    m_gridArea = 0;
    m_area_map = NULL;

    // Height level data
    m_gridHeight = INVALID_HEIGHT_VALUE;
    m_gridGetHeight = &GridMap::getHeightFromFlat;
    m_V9 = NULL;
    m_V8 = NULL;
    memset(m_holes, 0, sizeof(m_holes));

    // Liquid data
    m_liquidType = 0;
    m_liquid_offX = 0;
    m_liquid_offY = 0;
    m_liquid_width = 0;
    m_liquid_height = 0;
    m_liquidLevel = INVALID_HEIGHT_VALUE;
    m_liquidFlags = NULL;
    m_liquidEntry = NULL;
    m_liquid_map = NULL;
}

GridMap::~GridMap()
{
    unloadData();
}

bool GridMap::loadData(char* filename)
{
    // Unload old data if exist
    unloadData();

    GridMapFileHeader header;
    // Not return error if file not found
    FILE* in = nullptr;
    fopen_s(&in, filename, "rb");
    if (!in)
    {
        return true;
    }

    fread(&header, sizeof(header), 1, in);
    if (header.mapMagic == *((unsigned int const*)(MAP_MAGIC)) &&
        header.versionMagic == *((unsigned int const*)(MAP_VERSION_MAGIC)) &&
        IsAcceptableClientBuild(header.buildMagic))
    {
        // loadup area data
        if (header.areaMapOffset && !loadAreaData(in, header.areaMapOffset, header.areaMapSize))
        {
#if 0
            sLog.outError("Error loading map area data\n");
#endif
            fclose(in);
            return false;
        }

        // loadup holes data
        if (header.holesOffset && !loadHolesData(in, header.holesOffset, header.holesSize))
        {
#if 0
            sLog.outError("Error loading map holes data\n");
#endif
            fclose(in);
            return false;
        }

        // loadup height data
        if (header.heightMapOffset && !loadHeightData(in, header.heightMapOffset, header.heightMapSize))
        {
#if 0
            sLog.outError("Error loading map height data\n");
#endif
            fclose(in);
            return false;
        }

        // loadup liquid data
        if (header.liquidMapOffset && !loadGridMapLiquidData(in, header.liquidMapOffset, header.liquidMapSize))
        {
#if 0
            sLog.outError("Error loading map liquids data\n");
#endif
            fclose(in);
            return false;
        }

        fclose(in);
        return true;
    }

#if 0
    sLog.outError("Map file '%s' is non-compatible version created with a different map-extractor version.", filename);
#endif
    fclose(in);
    return false;
}

void GridMap::unloadData()
{
    delete[] m_area_map;
    delete[] m_V9;
    delete[] m_V8;
    delete[] m_liquidEntry;
    delete[] m_liquidFlags;
    delete[] m_liquid_map;

    m_area_map = NULL;
    m_V9 = NULL;
    m_V8 = NULL;
    m_liquidEntry = NULL;
    m_liquidFlags = NULL;
    m_liquid_map = NULL;
    m_gridGetHeight = &GridMap::getHeightFromFlat;
}

bool GridMap::loadAreaData(FILE* in, unsigned int offset, unsigned int /*size*/)
{
    GridMapAreaHeader header;
    fseek(in, offset, SEEK_SET);
    size_t file_read = fread(&header, sizeof(header), 1, in);
    if (file_read <= 0)
    {
        return false;
    }
    if (header.fourcc != *((unsigned int const*)(MAP_AREA_MAGIC)))
    {
        return false;
    }

    m_gridArea = header.gridArea;
    if (!(header.flags & MAP_AREA_NO_AREA))
    {
        m_area_map = new unsigned short[16 * 16];
        file_read = fread(m_area_map, sizeof(unsigned short), 16 * 16, in);
        if (file_read <= 0)
        {
            return false;
        }
    }

    return true;
}

bool GridMap::loadHeightData(FILE* in, unsigned int offset, unsigned int /*size*/)
{
    GridMapHeightHeader header;
    fseek(in, offset, SEEK_SET);
    size_t file_read = fread(&header, sizeof(header), 1, in);
    if (file_read <= 0)
    {
        return false;
    }
    if (header.fourcc != *((unsigned int const*)(MAP_HEIGHT_MAGIC)))
    {
        return false;
    }

    m_gridHeight = header.gridHeight;
    if (!(header.flags & MAP_HEIGHT_NO_HEIGHT))
    {
        if ((header.flags & MAP_HEIGHT_AS_INT16))
        {
            m_uint16_V9 = new unsigned short[129 * 129];
            m_uint16_V8 = new unsigned short[128 * 128];
            file_read = fread(m_uint16_V9, sizeof(unsigned short), 129 * 129, in);
            if (file_read <= 0)
            {
                return false;
            }
            file_read = fread(m_uint16_V8, sizeof(unsigned short), 128 * 128, in);
            if (file_read <= 0)
            {
                return false;
            }
            m_gridIntHeightMultiplier = (header.gridMaxHeight - header.gridHeight) / 65535;
            m_gridGetHeight = &GridMap::getHeightFromUInt16;
        }
        else if ((header.flags & MAP_HEIGHT_AS_INT8))
        {
            m_uint8_V9 = new unsigned char[129 * 129];
            m_uint8_V8 = new unsigned char[128 * 128];
            file_read = fread(m_uint8_V9, sizeof(unsigned char), 129 * 129, in);
            if (file_read <= 0)
            {
                return false;
            }
            file_read = fread(m_uint8_V8, sizeof(unsigned char), 128 * 128, in);
            if (file_read <= 0)
            {
                return false;
            }
            m_gridIntHeightMultiplier = (header.gridMaxHeight - header.gridHeight) / 255;
            m_gridGetHeight = &GridMap::getHeightFromUInt8;
        }
        else
        {
            m_V9 = new float[129 * 129];
            m_V8 = new float[128 * 128];
            file_read = fread(m_V9, sizeof(float), 129 * 129, in);
            if (file_read <= 0)
            {
                return false;
            }
            file_read = fread(m_V8, sizeof(float), 128 * 128, in);
            if (file_read <= 0)
            {
                return false;
            }
            m_gridGetHeight = &GridMap::getHeightFromFloat;
        }
    }
    else
    {
        m_gridGetHeight = &GridMap::getHeightFromFlat;
    }

    return true;
}

bool GridMap::loadHolesData(FILE* in, unsigned int offset, unsigned int /*size*/)
{
    if (fseek(in, offset, SEEK_SET) != 0)
    {
        return false;
    }

    if (fread(&m_holes, sizeof(m_holes), 1, in) != 1)
    {
        return false;
    }
    return true;
}

bool GridMap::loadGridMapLiquidData(FILE* in, unsigned int offset, unsigned int /*size*/)
{
    GridMapLiquidHeader header;
    fseek(in, offset, SEEK_SET);
    size_t file_read = fread(&header, sizeof(header), 1, in);
    if (file_read <= 0)
    {
        return false;
    }
    if (header.fourcc != *((unsigned int const*)(MAP_LIQUID_MAGIC)))
    {
        return false;
    }

    m_liquidType = header.liquidType;
    m_liquid_offX = header.offsetX;
    m_liquid_offY = header.offsetY;
    m_liquid_width = header.width;
    m_liquid_height = header.height;
    m_liquidLevel = header.liquidLevel;

    if (!(header.flags & MAP_LIQUID_NO_TYPE))
    {
        m_liquidEntry = new unsigned short[16 * 16];
        file_read = fread(m_liquidEntry, sizeof(unsigned short), 16 * 16, in);
        if (file_read <= 0)
        {
            return false;
        }

        m_liquidFlags = new unsigned char[16 * 16];
        file_read = fread(m_liquidFlags, sizeof(unsigned char), 16 * 16, in);
        if (file_read <= 0)
        {
            return false;
        }
    }

    if (!(header.flags & MAP_LIQUID_NO_HEIGHT))
    {
        m_liquid_map = new float[m_liquid_width * m_liquid_height];
        file_read = fread(m_liquid_map, sizeof(float), m_liquid_width * m_liquid_height, in);
        if (file_read <= 0)
        {
            return false;
        }
    }

    return true;
}

unsigned short GridMap::getArea(float x, float y)
{
    if (!m_area_map)
    {
        return m_gridArea;
    }

    x = 16 * (32 - x / SIZE_OF_GRIDS);
    y = 16 * (32 - y / SIZE_OF_GRIDS);
    int lx = (int)x & 15;
    int ly = (int)y & 15;
    return m_area_map[lx * 16 + ly];
}

float GridMap::getHeightFromFlat(float /*x*/, float /*y*/) const
{
    return m_gridHeight;
}

bool GridMap::isHole(int row, int col) const
{
    int cellRow = row / 8;     // 8 squares per cell
    int cellCol = col / 8;
    int holeRow = row % 8 / 2;
    int holeCol = (col - (cellCol * 8)) / 2;

    unsigned short hole = m_holes[cellRow][cellCol];

    return (hole & holetab_h[holeCol] & holetab_v[holeRow]) != 0;
}

float GridMap::getHeightFromFloat(float x, float y) const
{
    if (!m_V8 || !m_V9)
    {
        return INVALID_HEIGHT_VALUE;
    }

    x = MAP_RESOLUTION * (32 - x / SIZE_OF_GRIDS);
    y = MAP_RESOLUTION * (32 - y / SIZE_OF_GRIDS);

    int x_int = (int)x;
    int y_int = (int)y;
    x -= x_int;
    y -= y_int;
    x_int &= (MAP_RESOLUTION - 1);
    y_int &= (MAP_RESOLUTION - 1);

    if (isHole(x_int, y_int))
    {
        return INVALID_HEIGHT_VALUE;
    }

    // Height stored as: h5 - its v8 grid, h1-h4 - its v9 grid
    // +--------------> X
    // | h1-------h2     Coordinates is:
    // | | \  1  / |     h1 0,0
    // | |  \   /  |     h2 0,1
    // | | 2  h5 3 |     h3 1,0
    // | |  /   \  |     h4 1,1
    // | | /  4  \ |     h5 1/2,1/2
    // | h3-------h4
    // V Y
    // For find height need
    // 1 - detect triangle
    // 2 - solve linear equation from triangle points
    // Calculate coefficients for solve h = a*x + b*y + c

    float a, b, c;
    // Select triangle:
    if (x + y < 1)
    {
        if (x > y)
        {
            // 1 triangle (h1, h2, h5 points)
            float h1 = m_V9[(x_int) * 129 + y_int];
            float h2 = m_V9[(x_int + 1) * 129 + y_int];
            float h5 = 2 * m_V8[x_int * 128 + y_int];
            a = h2 - h1;
            b = h5 - h1 - h2;
            c = h1;
        }
        else
        {
            // 2 triangle (h1, h3, h5 points)
            float h1 = m_V9[x_int * 129 + y_int];
            float h3 = m_V9[x_int * 129 + y_int + 1];
            float h5 = 2 * m_V8[x_int * 128 + y_int];
            a = h5 - h1 - h3;
            b = h3 - h1;
            c = h1;
        }
    }
    else
    {
        if (x > y)
        {
            // 3 triangle (h2, h4, h5 points)
            float h2 = m_V9[(x_int + 1) * 129 + y_int];
            float h4 = m_V9[(x_int + 1) * 129 + y_int + 1];
            float h5 = 2 * m_V8[x_int * 128 + y_int];
            a = h2 + h4 - h5;
            b = h4 - h2;
            c = h5 - h4;
        }
        else
        {
            // 4 triangle (h3, h4, h5 points)
            float h3 = m_V9[(x_int) * 129 + y_int + 1];
            float h4 = m_V9[(x_int + 1) * 129 + y_int + 1];
            float h5 = 2 * m_V8[x_int * 128 + y_int];
            a = h4 - h3;
            b = h3 + h4 - h5;
            c = h5 - h4;
        }
    }
    // Calculate height
    return a * x + b * y + c;
}

float GridMap::getHeightFromUInt8(float x, float y) const
{
    if (!m_uint8_V8 || !m_uint8_V9)
    {
        return m_gridHeight;
    }

    x = MAP_RESOLUTION * (32 - x / SIZE_OF_GRIDS);
    y = MAP_RESOLUTION * (32 - y / SIZE_OF_GRIDS);

    int x_int = (int)x;
    int y_int = (int)y;
    x -= x_int;
    y -= y_int;
    x_int &= (MAP_RESOLUTION - 1);
    y_int &= (MAP_RESOLUTION - 1);

    int a, b, c;
    unsigned char* V9_h1_ptr = &m_uint8_V9[x_int * 128 + x_int + y_int];
    if (x + y < 1)
    {
        if (x > y)
        {
            // 1 triangle (h1, h2, h5 points)
            int h1 = V9_h1_ptr[0];
            int h2 = V9_h1_ptr[129];
            int h5 = 2 * m_uint8_V8[x_int * 128 + y_int];
            a = h2 - h1;
            b = h5 - h1 - h2;
            c = h1;
        }
        else
        {
            // 2 triangle (h1, h3, h5 points)
            int h1 = V9_h1_ptr[0];
            int h3 = V9_h1_ptr[1];
            int h5 = 2 * m_uint8_V8[x_int * 128 + y_int];
            a = h5 - h1 - h3;
            b = h3 - h1;
            c = h1;
        }
    }
    else
    {
        if (x > y)
        {
            // 3 triangle (h2, h4, h5 points)
            int h2 = V9_h1_ptr[129];
            int h4 = V9_h1_ptr[130];
            int h5 = 2 * m_uint8_V8[x_int * 128 + y_int];
            a = h2 + h4 - h5;
            b = h4 - h2;
            c = h5 - h4;
        }
        else
        {
            // 4 triangle (h3, h4, h5 points)
            int h3 = V9_h1_ptr[1];
            int h4 = V9_h1_ptr[130];
            int h5 = 2 * m_uint8_V8[x_int * 128 + y_int];
            a = h4 - h3;
            b = h3 + h4 - h5;
            c = h5 - h4;
        }
    }

    // Calculate height
    return (float)((a * x) + (b * y) + c) * m_gridIntHeightMultiplier + m_gridHeight;
}

float GridMap::getHeightFromUInt16(float x, float y) const
{
    if (!m_uint16_V8 || !m_uint16_V9)
    {
        return m_gridHeight;
    }

    x = MAP_RESOLUTION * (32 - x / SIZE_OF_GRIDS);
    y = MAP_RESOLUTION * (32 - y / SIZE_OF_GRIDS);

    int x_int = (int)x;
    int y_int = (int)y;
    x -= x_int;
    y -= y_int;
    x_int &= (MAP_RESOLUTION - 1);
    y_int &= (MAP_RESOLUTION - 1);

    int a, b, c;
    unsigned short* V9_h1_ptr = &m_uint16_V9[x_int * 128 + x_int + y_int];
    if (x + y < 1)
    {
        if (x > y)
        {
            // 1 triangle (h1, h2, h5 points)
            int h1 = V9_h1_ptr[0];
            int h2 = V9_h1_ptr[129];
            int h5 = 2 * m_uint16_V8[x_int * 128 + y_int];
            a = h2 - h1;
            b = h5 - h1 - h2;
            c = h1;
        }
        else
        {
            // 2 triangle (h1, h3, h5 points)
            int h1 = V9_h1_ptr[0];
            int h3 = V9_h1_ptr[1];
            int h5 = 2 * m_uint16_V8[x_int * 128 + y_int];
            a = h5 - h1 - h3;
            b = h3 - h1;
            c = h1;
        }
    }
    else
    {
        if (x > y)
        {
            // 3 triangle (h2, h4, h5 points)
            int h2 = V9_h1_ptr[129];
            int h4 = V9_h1_ptr[130];
            int h5 = 2 * m_uint16_V8[x_int * 128 + y_int];
            a = h2 + h4 - h5;
            b = h4 - h2;
            c = h5 - h4;
        }
        else
        {
            // 4 triangle (h3, h4, h5 points)
            int h3 = V9_h1_ptr[1];
            int h4 = V9_h1_ptr[130];
            int h5 = 2 * m_uint16_V8[x_int * 128 + y_int];
            a = h4 - h3;
            b = h3 + h4 - h5;
            c = h5 - h4;
        }
    }

    // Calculate height
    return (float)((a * x) + (b * y) + c) * m_gridIntHeightMultiplier + m_gridHeight;
}

float GridMap::getLiquidLevel(float x, float y)
{
    if (!m_liquid_map)
    {
        return m_liquidLevel;
    }

    x = MAP_RESOLUTION * (32 - x / SIZE_OF_GRIDS);
    y = MAP_RESOLUTION * (32 - y / SIZE_OF_GRIDS);

    int cx_int = ((int)x & (MAP_RESOLUTION - 1)) - m_liquid_offY;
    int cy_int = ((int)y & (MAP_RESOLUTION - 1)) - m_liquid_offX;

    if (cx_int < 0 || cx_int >= m_liquid_height)
    {
        return INVALID_HEIGHT_VALUE;
    }

    if (cy_int < 0 || cy_int >= m_liquid_width)
    {
        return INVALID_HEIGHT_VALUE;
    }

    return m_liquid_map[cx_int * m_liquid_width + cy_int];
}

unsigned char GridMap::getTerrainType(float x, float y)
{
    if (!m_liquidFlags)
    {
        return (unsigned char)m_liquidType;
    }

    x = 16 * (32 - x / SIZE_OF_GRIDS);
    y = 16 * (32 - y / SIZE_OF_GRIDS);
    int lx = (int)x & 15;
    int ly = (int)y & 15;
    return m_liquidFlags[lx * 16 + ly];
}

// Get water state on map
GridMapLiquidStatus GridMap::getLiquidStatus(float x, float y, float z, unsigned char ReqLiquidType, GridMapLiquidData* data)
{
#if 0
    // Check water type (if no water return)
    if (!m_liquidFlags && !m_liquidType)
    {
        return LIQUID_MAP_NO_WATER;
    }

    // Get cell
    float cx = MAP_RESOLUTION * (32 - x / SIZE_OF_GRIDS);
    float cy = MAP_RESOLUTION * (32 - y / SIZE_OF_GRIDS);

    int x_int = (int)cx & (MAP_RESOLUTION - 1);
    int y_int = (int)cy & (MAP_RESOLUTION - 1);

    // Check water type in cell
    int idx = (x_int >> 3) * 16 + (y_int >> 3);
    unsigned char type = m_liquidFlags ? m_liquidFlags[idx] : 1 << m_liquidType;
    unsigned int entry = 0;

    // ToDo: check if this part requires update for 1.12.1
    if (m_liquidEntry)
    {
        if (LiquidTypeEntry const* liquidEntry = sLiquidTypeStore.LookupEntry(m_liquidEntry[idx]))
        {
            entry = liquidEntry->Id;
            type &= MAP_LIQUID_TYPE_DARK_WATER;
            unsigned int liqTypeIdx = liquidEntry->Type;
            if ((entry < 21) && (type & MAP_LIQUID_TYPE_WATER))
            {
                // only basic liquid stored in maps actualy so in some case we need to override type depend on area
                // actualy only Naxxramas raid be overrided here
                if (AreaTableEntry const* area = sAreaStore.LookupEntry(getArea(x, y)))
                {
                    unsigned int overrideLiquid = area->LiquidTypeOverride;
                    if (!overrideLiquid && area->zone)
                    {
                        area = GetAreaEntryByAreaID(area->zone);
                        if (area)
                        {
                            overrideLiquid = area->LiquidTypeOverride;
                        }
                    }

                    if (LiquidTypeEntry const* liq = sLiquidTypeStore.LookupEntry(overrideLiquid))
                    {
                        entry = overrideLiquid;
                        liqTypeIdx = liq->Type;
                    }
                }
            }

            type |= (1 << liqTypeIdx) | (type & MAP_LIQUID_TYPE_DARK_WATER);
        }
    }

    if (type == 0)
    {
        return LIQUID_MAP_NO_WATER;
    }

    // Check req liquid type mask
    if (ReqLiquidType && !(ReqLiquidType & type))
    {
        return LIQUID_MAP_NO_WATER;
    }

    // Check water level:
    // Check water height map
    int lx_int = x_int - m_liquid_offY;
    if (lx_int < 0 || lx_int >= m_liquid_height)
    {
        return LIQUID_MAP_NO_WATER;
    }

    int ly_int = y_int - m_liquid_offX;
    if (ly_int < 0 || ly_int >= m_liquid_width)
    {
        return LIQUID_MAP_NO_WATER;
    }

    // Get water level
    float liquid_level = m_liquid_map ? m_liquid_map[lx_int * m_liquid_width + ly_int] : m_liquidLevel;

    // Get ground level (sub 0.2 for fix some errors)
    float ground_level = getHeight(x, y);

    // Check water level and ground level
    if (liquid_level < ground_level || z < ground_level - 2)
    {
        return LIQUID_MAP_NO_WATER;
    }

    // All ok in water -> store data
    if (data)
    {
        data->entry = entry;
        data->type_flags = type;
        data->level = liquid_level;
        data->depth_level = ground_level;
    }

    // For speed check as int values
    int delta = int((liquid_level - z) * 10);

    // Get position delta
    if (delta > 20)                                         // Under water
    {
        return LIQUID_MAP_UNDER_WATER;
    }

    if (delta > 0)                                          // In water
    {
        return LIQUID_MAP_IN_WATER;
    }

    if (delta > -1)                                         // Walk on water
    {
        return LIQUID_MAP_WATER_WALK;
    }
#endif
    // Above water
    return LIQUID_MAP_ABOVE_WATER;
}

bool GridMap::ExistMap(unsigned int mapid, int gx, int gy)
{
#if 0
    int len = sWorld.GetDataPath().length() + strlen("maps/%03u%02u%02u.map") + 1;
    char* tmp = new char[len];
    snprintf(tmp, len, (char*)(sWorld.GetDataPath() + "maps/%03u%02u%02u.map").c_str(), mapid, gx, gy);

    FILE* pf = fopen(tmp, "rb");

    if (!pf)
    {
        sLog.outError("Please check for the existence of map file '%s'", tmp);
        delete[] tmp;
        return false;
    }

    GridMapFileHeader header;
    size_t file_read = fread(&header, sizeof(header), 1, pf);
    if (file_read <= 0)
    {
        sLog.outError("Map file '%s' could not be read.", tmp);
        delete[] tmp;
        fclose(pf);                                         // close file before return
        return false;
    }
    if (header.mapMagic != *((unsigned int const*)(MAP_MAGIC)) ||
        header.versionMagic != *((unsigned int const*)(MAP_VERSION_MAGIC)) ||
        !IsAcceptableClientBuild(header.buildMagic))
    {
        sLog.outError("Map file '%s' is non-compatible version created with a different map-extractor version.", tmp);
        delete[] tmp;
        fclose(pf);                                         // close file before return
        return false;
    }

    delete[] tmp;
    fclose(pf);
#endif
    return true;
}

bool GridMap::ExistVMap(unsigned int mapid, int gx, int gy)
{
#if 0
    if (VMAP::IVMapManager* vmgr = VMAP::VMapFactory::createOrGetVMapManager())
    {
        if (vmgr->isMapLoadingEnabled())
        {
            // x and y are swapped !! => fixed now
            bool exists = vmgr->existsMap((sWorld.GetDataPath() + "vmaps").c_str(), mapid, gx, gy);
            if (!exists)
            {
                std::string name = vmgr->getDirFileName(mapid, gx, gy);
                sLog.outError("VMap file '%s' is missing or point to wrong version vmap file, redo vmaps with latest vmap_assembler.exe program", (sWorld.GetDataPath() + "vmaps/" + name).c_str());
                return false;
            }
        }
    }
#endif
    return true;
}